import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.XMLConstants;
import javax.xml.validation.*;
import javax.xml.parsers.*;
import org.xml.sax.SAXException;
import org.w3c.dom.*;
import org.json.JSONObject;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.*;

import org.eclipse.jetty.server.Request;
import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;

import java.sql.SQLException;

final public class SEMDValidator extends HttpServlet {

    // Constants
    final private static Logger log = LogManager.getLogger(SEMDValidator.class.getName());
    final private static MultipartConfigElement MULTI_PART_CONFIG = new MultipartConfigElement(
            System.getProperty("java.io.tmpdir"));

    // Settings from web.xml
    private String DATA_PATH;
    private String ADMIN_NAME;
    private String ADMIN_PASS;
    private String LIST_TYPES_FOR_VARIFICATION[] = {};

    // Значения, загружаемые из файла FNSITags.txt
    private CopyOnWriteArrayList<String> LIST_TAGS_FOR_VARIFICATION = new CopyOnWriteArrayList<String>();

    // In memory db
    private HashMap<String, Templates> cacheXslt = new HashMap<String, Templates>(100);
    private HashMap<String, Schema> cacheXsd = new HashMap<String, Schema>(100);

    private RefManager rm = null;

    public void init() throws ServletException {
        System.setProperty("javax.xml.transform.TransformerFactory", "net.sf.saxon.TransformerFactoryImpl");
        System.setProperty("javax.xml.parsers.DocumentBuilderFactory",
                "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
        ServletConfig config = this.getServletConfig();

        String param = config.getInitParameter("LIST_TYPES_FOR_VARIFICATION");
        if (param != null && param.length() > 0) {
            String[] types = param.split(",");
            LIST_TYPES_FOR_VARIFICATION = new String[types.length];
            for (int i = 0; i < types.length; i++) {
                LIST_TYPES_FOR_VARIFICATION[i] = types[i].trim();
            }
            Arrays.sort(LIST_TYPES_FOR_VARIFICATION);
        }
        log.info("LIST_TYPES_FOR_VARIFICATION=" + Arrays.toString(LIST_TYPES_FOR_VARIFICATION));

        DATA_PATH = config.getInitParameter("DATA_PATH");
        if (DATA_PATH == null)
            throw new ServletException("Servlet param DATA_PATH is not set");

        if (DATA_PATH.charAt(DATA_PATH.length() - 1) == '/' || DATA_PATH.charAt(DATA_PATH.length() - 1) == '\\')
            DATA_PATH = DATA_PATH.substring(0, DATA_PATH.length() - 1);
        log.info("DATA_PATH=" + DATA_PATH);

        ADMIN_NAME = config.getInitParameter("ADMIN_NAME");
        if (ADMIN_NAME == null)
            throw new ServletException("Servlet param ADMIN_NAME is not set");
        ADMIN_PASS = config.getInitParameter("ADMIN_PASS");
        if (ADMIN_PASS == null)
            throw new ServletException("Servlet param ADMIN_PASS is not set");

        String conn = config.getInitParameter("DB");
        log.info("DB=" + conn);

        try {
            Class.forName("org.sqlite.JDBC");
        } catch (ClassNotFoundException ex) {
            log.error("org.sqlite.JDBC class not found");
        }
        rm = new RefManager(DATA_PATH + "/" + "avl/", "jdbc:sqlite:"+conn);

        try {
            loadFNSITags(null);
        } catch (IOException e) {
            throw new ServletException("loadFNSITags exception - " + e.getMessage());
        }
    }

    // удаляем файлы из fnsi, доступа к которым не было более 7 дней.
    @Override
    public void destroy() {
        log.info("destroy");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        HttpSession session = req.getSession(false);
        boolean user = false;
        if (isLocal(req) || (session != null && session.getAttribute("user") != null)) {
            user = true;
        }
        if (req.getServletPath().equals("/send_sch.html")) {
            log.info(req.getServletPath() + " from " + req.getRemoteAddr());
            if (user)
                send_sch(req.getContextPath(), resp);
            else
                loginHtml(req.getContextPath(), resp);
        } else if (req.getServletPath().equals("/get_sch_list.html")) {
            log.info(req.getServletPath() + " from " + req.getRemoteAddr());
            if (user)
                get_sch_list(req.getContextPath(), resp);
            else
                loginHtml(req.getContextPath(), resp);
        } else if (req.getServletPath().equals("/send_semd.html")) {
            log.info(req.getServletPath());
            send_semd(req.getContextPath(), resp);
        } else if (req.getServletPath().equals("/data")) {
            log.info(req.getQueryString());
            data(req.getQueryString(), resp);
        } else if (req.getServletPath().startsWith("/updatedb/")) {
            log.info(req.getServletPath() + " from " + req.getRemoteAddr());
            if (user)
                updatedb(req.getServletPath(), resp);
            else
                resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Authorization requied");
        } else if (req.getServletPath().startsWith("/reloadRefSetting/")) {
            log.info(req.getServletPath() + " from " + req.getRemoteAddr());
            if (user)
                reloadRefSetting(req.getServletPath(), resp);
            else
                resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Authorization requied");
        } else if (req.getServletPath().equals("/reloadFNSIEMD")) {
            log.info(req.getServletPath() + " from " + req.getRemoteAddr());
            if (user)
                reloadFNSIEMD(req.getServletPath(), resp);
            else
                resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Authorization requied");
        } else if (req.getServletPath().startsWith("/reloadRefData/")) {
            log.info(req.getServletPath() + " from " + req.getRemoteAddr());
            if (user)
                reloadRefData(req.getServletPath(), resp);
            else
                resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Authorization requied");
        } else if (req.getServletPath().equals("/login")) {
            log.info(req.getServletPath());
            login(req, resp);
        } else if (req.getServletPath().equals("/login.html")) {
            log.info(req.getServletPath());
            loginHtml(req.getContextPath(), resp);
        } else if (req.getServletPath().startsWith("/file/")) {
            log.info(req.getServletPath());
            download(req.getContextPath(), req.getServletPath(), resp);
        } else {
            log.warn("PAGE NOT FOUND: " + req.getServletPath());
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        if (req.getServletPath().equals("/verify")) {
            log.info(req.getServletPath());
            verify(req, resp);
        } else if (req.getServletPath().equals("/upload")) {
            log.info(req.getServletPath() + " from " + req.getRemoteAddr());
            HttpSession session = req.getSession(false);
            if (isLocal(req) || session != null && session.getAttribute("user") != null)
                upload(req, resp);
            else
                resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Authorization Failed");
        } else if (req.getServletPath().equals("/uploadFNSITags")) {
            log.info(req.getServletPath() + " from " + req.getRemoteAddr());
            HttpSession session = req.getSession(false);
            if (isLocal(req) || session != null && session.getAttribute("user") != null)
                uploadFNSITags(req, resp);
            else
                resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Authorization Failed");
        } else {
            log.warn("PAGE NOT FOUND: " + req.getServletPath());
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }

    private void reloadRefSetting(final String servletPath, HttpServletResponse resp)
            throws IOException, ServletException {

        resp.setHeader("Content-Type", "text/plain; charset=UTF-8");
        PrintWriter out = resp.getWriter();
        String[] url = servletPath.split("/");
        String oid = url[url.length - 1];

        if (rm.clearRefSetting(oid)) {
            out.print("OK");
        } else {
            out.print("Reference '" + oid + "' not loaded");
        }
    }

    private void reloadRefData(final String servletPath, HttpServletResponse resp)
            throws IOException, ServletException {

        resp.setHeader("Content-Type", "text/plain; charset=UTF-8");
        PrintWriter out = resp.getWriter();
        String[] url = servletPath.split("/");
        String oid = url[url.length - 1];

        if (rm.clearRefData(oid)) {
            out.print("OK");
        } else {
            out.print("Reference '" + oid + "' not loaded");
        }
    }

    private void reloadFNSIEMD(final String servletPath, HttpServletResponse resp)
            throws IOException, ServletException {

        resp.setHeader("Content-Type", "text/plain; charset=UTF-8");
        PrintWriter out = resp.getWriter();

        rm.clearFNSIEMD();
        out.print("OK ");
    }

    static private boolean isLocal(final HttpServletRequest request) {
        String ip = request.getHeader("X-Real-IP");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        if (ip.equals("127.0.0.1") || ip.equals("0:0:0:0:0:0:0:1"))
            return true;
        return false;
    }

    /**
     * Покахать html страницу авторизации
     * 
     * @param contextPath URL сайта для формирования гипер ссылок
     * @param resp        Поток для вывода результата
     * @throws IOException
     */
    private void loginHtml(final String contextPath, HttpServletResponse resp)
            throws IOException {

        resp.setHeader("Content-Type", "text/html; charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.print(String.join("\n", "<html>", "<head>", "<title>Вход в администрирование СЭМД валидатора</title>", "</head>",
                "<body>", "<H1>Вход в администрирование СЭМД валидатора</H1>",
                "<form method='get' action='" + contextPath + "/login'>", "  <fieldset>",
                "  <p><label for='user'>Имя:</label><input type='text' name='user'></p>",
                "  <p><label for='pass'>Пароль:</label><input type='password' name='pass'></p>", "</fieldset>", "<p>",
                " <input type='submit' value='Войти'>", " </p>", "</form>", "</body>", "</html>"));
    }

    /**
     * Выполнить авторизацию. Авторизация дает доступ к странице настроек и их
     * изменению.
     * 
     * @param req  Входящий поток (данные html формы)
     * @param resp Поток для вывода результата
     * @throws IOException
     */
    private void login(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String user = req.getParameter("user");
        String password = req.getParameter("pass");
        HttpSession session = req.getSession(true);

        if (user != null && password != null && user.equals(ADMIN_NAME) && password.equals(ADMIN_PASS)) {
            session.setAttribute("user", user);
            get_sch_list(req.getContextPath(), resp);
        } else {
            resp.setHeader("Content-Type", "text/html; charset=UTF-8");
            PrintWriter out = resp.getWriter();
            out.print(String.join("\n", "<html>", "<head>", "  <title>Ошибка авторизации</title>", "</head>",
                    " <body>", "  <H1>Ошибка авторизации</H1>", "</head>", "<body>"));
        }
    }

    /**
     * Загрузить СЭМД для валидации. СЭМД загружается через html форму, в которой
     * задаются параметры проверки - тип СЭМД и тип проверки
     * 
     * @param req  Входящий поток (данные html формы)
     * @param resp Поток для вывода результата
     * @throws IOException
     * @throws ServletException
     */
    private void verify(HttpServletRequest req, HttpServletResponse resp)
            throws IOException, ServletException {

        if (req.getContentType() != null && req.getContentType().startsWith("multipart/form-data")) {
            req.setAttribute(Request.MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG);
        }
        resp.setHeader("Content-Type", "text/plain; charset=UTF-8");
        PrintWriter out = resp.getWriter();

        final Part filePart = req.getPart("file");
        final String remdtype = req.getParameter("remdtype");
        final String verifytype = req.getParameter("verifytype");

        try {
            int vt = Integer.parseInt(verifytype);
            if (vt < 0 || vt > 3) {
                out.print("verifytype parameter should be in (0, 1, 2, 3)");
                return;
            }
        } catch (NumberFormatException e) {
            out.print("verifytype parameter should be integer");
            return;
        }

        log.info("REMD type: " + remdtype + ", verify type: " + verifytype);
        InputStream fileContent = filePart.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(fileContent, "UTF-8"));
        String xml = reader.lines().collect(Collectors.joining("\n"));
        // remove BOM
        if (xml.startsWith("\uFEFF")) {
            out.print("XML should be without BOM");
            return;
        }

        if (xml.length() < 50) {
            out.print("XML is broken. Length=" + xml.length());
            return;
        }
        if (xml.substring(30, 50).indexOf("UTF-8") == -1) {
            out.print("First line should be '<?xml?>' with encoding='UTF-8'");
            return;
        }

        File xsd = new File(DATA_PATH + "/" + remdtype + "/CDA.xsd");
        boolean valid = true;
        Document document = null;
        boolean filtered = LIST_TYPES_FOR_VARIFICATION.length > 0
                && Arrays.binarySearch(LIST_TYPES_FOR_VARIFICATION, remdtype) < 0;
        if (filtered || !xsd.exists() || xsd.isDirectory()) {
            out.print("no xsd - " + remdtype + "/CDA.xsd");
            valid = false;
            if (filtered) {
                log.info("REMD type filtered: " + remdtype);
            }
        } else if (verifytype.equals("0") || verifytype.equals("2") || verifytype.equals("3")) { // xsd validation
            try {
                document = xsd(xml, xsd.getAbsolutePath());
            } catch (ParserConfigurationException e) {
                out.print(e.getMessage());
                valid = false;
            } catch (SAXException e) {
                out.print(e.getMessage());
                valid = false;
            }
        }

        // fnsi validation
        if (valid && verifytype.equals("3")) {
            for (String tag : LIST_TAGS_FOR_VARIFICATION) {
                NodeList codeList = document.getDocumentElement().getElementsByTagName(tag);
                for (int j = 0; j < codeList.getLength(); j++) {
                    NamedNodeMap attributes = codeList.item(j).getAttributes();
                    validateTag(out, tag, attributes, remdtype);
                }
            }
        }

        // xslt validation
        if (valid && !verifytype.equals("0")) {
            // remove namespace because schematron validator doesn't see data with any
            // default namespace
            xml = xml.replace("xmlns=\"urn:hl7-org:v3\"", "").replace("xmlns='urn:hl7-org:v3'", "");
            try {
                File folder = new File(DATA_PATH + "/schematrons");
                boolean found = false;
                StringBuffer res = new StringBuffer();
                for (File fileEntry : folder.listFiles()) {
                    if (!fileEntry.isDirectory()) {
                        final String filename = fileEntry.getName();
                        final String ext = filename.substring(filename.length() - 4, filename.length());
                        final String name = filename.substring(0, filename.length() - 4);
                        if ((name.equals(remdtype) || name.startsWith(remdtype + "_")) && ext.equals(".xsl")) {
                            StringWriter outWriter = new StringWriter();
                            StreamResult result = new StreamResult(outWriter);
                            xslt(new StreamSource(new StringReader(xml)), fileEntry.getAbsolutePath(), result);
                            StringBuffer sb = outWriter.getBuffer();

                            // вырезаем <?xml version="1.0" encoding="UTF-8"?>
                            if (sb.substring(0, 6).equals("<?xml ")) {
                                int i = sb.indexOf(">", 7) + 1;
                                res.append("<sch file=\"");
                                res.append(filename);
                                res.append("\">");
                                res.append(sb.substring(i));
                                res.append("</sch>");
                            }
                            found = true;
                        }
                    }
                }
                if (!found) {
                    out.print("error - no files '/schematrons/" + remdtype + "*.xsl'");
                } else {
                    out.print("<result>");
                    out.print(res.toString());
                    out.print("</result>");
                }
            } catch (TransformerException e) {
                out.print("error sch transfromation: " + e.getMessageAndLocation().replaceAll("\n", "<br>") + " "
                        + e.getLocator());
                valid = false;
            }
            reader.close();
        } else if (valid) { // для xsd проверки
            out.print("valid");
        }
    }

    /**
     * Загрузка zip архива с xsd и sch для конкретного типа СЭМД.
     * Требования к архиву описаны на странице с формой отправки zip (send_sch)
     * 
     * @param req  Входящий файл
     * @param resp Поток для вывода результата
     * @throws IOException
     * @throws ServletException
     */
    private void upload(HttpServletRequest req, HttpServletResponse resp)
            throws IOException, ServletException {

        if (req.getContentType() != null && req.getContentType().startsWith("multipart/form-data")) {
            req.setAttribute(Request.MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG);
        }
        resp.setHeader("Content-Type", "text/plain; charset=UTF-8");
        PrintWriter out = resp.getWriter();

        Part filePart = req.getPart("file");
        final String fileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString();
        if (fileName.length() < 5) {
            out.print("file name should be like '118.zip'");
            return;
        }
        final String remdtype = fileName.substring(0, fileName.length() - 4);
        InputStream fileContent = filePart.getInputStream();
        unzip(DATA_PATH, fileContent);

        try {
            if (!remdtype.startsWith("beta")) {
                if (Integer.parseInt(remdtype) < 1) {
                    out.print("remdtype in filename should be number or like 'beta%'");
                    return;
                }
            } else {
                if (remdtype.contains(" ") || remdtype.contains(".") || remdtype.contains("_")) {
                    throw new IOException("SEMD-beta type should not contains spaces, dots, Underscores");
                }
            }
        } catch (NumberFormatException e) {
            out.print("remdtype parameter should be integer");
            return;
        }

        File xsd = new File(DATA_PATH + "/" + remdtype + "/CDA.xsd");
        if (!xsd.exists() || xsd.isDirectory()) {
            out.print("no xsd - " + remdtype + "/CDA.xsd");
        } else {
            File folder = new File(DATA_PATH);
            boolean valid = true;
            boolean sch = false;
            for (final File fileEntry : folder.listFiles()) {
                if (!fileEntry.isDirectory()) {
                    final String filename = fileEntry.getName();
                    final String ext = filename.substring(filename.length() - 4, filename.length());
                    final String name = filename.substring(0, filename.length() - 4);
                    if ((name.equals(remdtype) || name.startsWith(remdtype + "_")) && ext.equals(".sch")) {
                        sch = true;
                        final String xslName = DATA_PATH + "/schematrons/" + name + ".xsl";
                        final File xsl = new File(xslName);
                        if (xsl.exists()) {
                            xsl.delete();
                            log.info("Delete file " + xsl);
                        }
                        final String style = getServletContext()
                                .getRealPath("/schxslt-1.9.1/2.0/pipeline-for-svrl.xsl");
                        if (style == null) {
                            throw new IOException("Style file not found");
                        }
                        try {
                            xslt(new StreamSource(fileEntry), style, new StreamResult(new FileOutputStream(xsl)));
                        } catch (TransformerException e) {
                            out.print("error sch transfromation: " + e.getMessage());
                            valid = false;
                        }
                    }
                }
            }
            if (valid && sch) {
                out.print("xsd and sch loaded");
                cacheXslt.clear();
                cacheXsd.clear();
            } else if (valid)
                out.print("xsd loaded");
            cacheXsd.clear();
        }
    }

    /**
     * Команда генерации xsl для схематронов. Файлы уже должны находится в папке
     * DATA_PATH. Требования к файлам такие же как как к отправляемым в zip'е
     * Команда доступна только с локалхоста
     * 
     * @param servletPath URL в котором передается список типов рэмд, которым
     *                    сгененировать XSL. Список передается через разделитель -
     *                    "_"
     * @param resp        Поток для вывода результата
     * @throws IOException
     */
    private void updatedb(final String servletPath, HttpServletResponse resp)
            throws IOException {
        resp.setHeader("Content-Type", "text/plain; charset=UTF-8");
        PrintWriter out = resp.getWriter();

        String[] url = servletPath.split("/");
        String params = url[url.length - 1];
        String[] remdtype_ar = params.split("_");
        List<String> done = new ArrayList<String>();

        boolean valid = true;
        File folder = new File(DATA_PATH);
        for (final File fileEntry : folder.listFiles()) {
            if (!fileEntry.isDirectory()) {
                final String filename = fileEntry.getName();
                final String ext = filename.substring(filename.length() - 4, filename.length());
                final String name = filename.substring(0, filename.length() - 4);
                for (String remdtype : remdtype_ar) {
                    if ((name.equals(remdtype) || name.startsWith(remdtype + "_")) && ext.equals(".sch")) {
                        final String xslName = DATA_PATH + "/schematrons/" + name + ".xsl";
                        final File xsl = new File(xslName);
                        if (xsl.exists()) {
                            xsl.delete();
                            log.info("Delete file " + xsl);
                        }
                        final String style = getServletContext()
                                .getRealPath("/schxslt-1.9.1/2.0/pipeline-for-svrl.xsl");
                        if (style == null) {
                            throw new IOException("Style file not found");
                        }
                        try {
                            xslt(new StreamSource(fileEntry), style, new StreamResult(new FileOutputStream(xsl)));
                            done.add(remdtype);
                        } catch (TransformerException e) {
                            out.print("error sch transfromation: " + e.getMessage());
                            valid = false;
                        }
                    }
                }
            }
        }
        if (valid) {
            cacheXslt.clear();
            cacheXsd.clear();
            out.print("OK ");
            out.print(String.join(",", done));
        }
    }

    /**
     * Загрузка текстового файла со списков тегов для проверки на соотвествие
     * справочникам ФНСИ
     * 
     * @param req  Входящий файл
     * @param resp Поток для вывода результата
     * @throws IOException
     * @throws ServletException
     */
    private void uploadFNSITags(HttpServletRequest req, HttpServletResponse resp)
            throws IOException, ServletException {

        if (req.getContentType() != null && req.getContentType().startsWith("multipart/form-data")) {
            req.setAttribute(Request.MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG);
        }
        resp.setHeader("Content-Type", "text/plain; charset=UTF-8");
        PrintWriter out = resp.getWriter();

        Part filePart = req.getPart("file");
        final File file = new File(DATA_PATH + "/FNSITags.txt");
        InputStream fileContent = filePart.getInputStream();

        byte[] buffer = new byte[2048];
        FileOutputStream fOutput = new FileOutputStream(file);
        int count = 0;
        while ((count = fileContent.read(buffer)) > 0) {
            fOutput.write(buffer, 0, count);
        }
        fOutput.close();

        loadFNSITags(out);

        out.print("FNSITags.txt loaded");
    }

    /**
     * HTML страница с формами отправки файлов настроек в сервис валидации, в т.ч.
     * xsd и sch схемы
     * 
     * @param contextPath URL сайта для формирования ссылок
     * @param resp        Поток для вывода результата
     * @throws IOException
     */
    private void send_sch(final String contextPath, HttpServletResponse resp)
            throws IOException {

        resp.setHeader("Content-Type", "text/html; charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.print(String.join("\n", "<html>", " <head>", "  <title>Настройка</title>", " </head>", " <body>",
                "  <H1>Отправка zip-архива со схемой и схематроном</H1>",
                "  <form enctype = 'multipart/form-data'  method='post' action='" + contextPath + "/upload'>",
                "   <input type='file' name='file'/>", "   <input type='submit' value='Отправить zip'/>", "  </form>",
                "<hr>", "<h3>Структура zip-архива</h3>", "<ul>", "<li>Имя архива - 118.zip, где имя файла - ",
                "тип РЭМД (OID)</li><li>118_1.sch - схематрон (не обязателен). Первое число - тип РЭМД (OID),",
                "второе число (необязательное) используется в случае, когда для типа РЭМД существует несколько схематронов</li>",
                "<li>118 - папка (обязательна). Число - тип РЭМД (OID).</li>", "<ul>",
                "<li>CDA.xsd - схема (обязательная). Имя файла должно быть именно таким</li>",
                "<li>... - другие xsd файл, на которые ссылается головная схема. Допустимы папки, оригинальная иерархия должна быть сохранена</li>",
                "</ul>",
                "<li>Второй вариант имени архива - betaOnko.zip. Используется для СЭМД-бета, имя должно начинаться на 'beta' и не содержать символов '.', '_', ' '.<br>",
                "В этом случае обязательная папка будет иметь название betaOnko. Схемарон (при наличии) также будет иметь имя betaOnko.sch или betaOnko_1.sch</li>",
                "</ul>", "<hr>",
                "  <H1>Отправка файла со списком тегов, подлежащих контролю на соответствие справочникам ФНСИ</H1>",
                "  <form enctype = 'multipart/form-data'  method='post' action='" + contextPath + "/uploadFNSITags'>",
                "   <input type='file' name='file'/>", "   <input type='submit' value='Отправить FNSITags.txt'/>",
                "  </form>", "<h2>См. также</h2>",
                "<a href='" + contextPath + "/send_semd.html'>Валидация СЭМД</a><br>",
                "<a href='" + contextPath + "/get_sch_list.html'>Просмотр загруженных схем и схематронов</a><br>",
                "</body>", "</html>"));
    }

    /**
     * Вывеcти html страницу, содержащую xsd схему.
     * 
     * @param contextPath URL сайта для формирования ссылок
     * @param servletPath URL xsd, которая быдет выведена в html
     * @param resp        Поток для вывода результата
     * @throws IOException
     */
    private void download(final String contextPath, final String servletPath, HttpServletResponse resp)
            throws IOException {

        resp.setHeader("Content-Type", "text/html; charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.print(String.join("\n", "<html>", " <head>", "  <title>", servletPath.substring(5), "</title>", " </head>",
                " <body>"));

        if (servletPath.contains("..")) {
            out.print(":ERROR: URL '" + servletPath + "' contains '..'");
        } else {
            final String url = DATA_PATH + servletPath.substring(5); // 5 - это "/file"
            final int i = servletPath.lastIndexOf("/"); // remove file name
            final String p = contextPath + "/file" + servletPath.substring(5, i + 1); // 5 - это "/file"
            final File file = new File(url);
            if (file.exists()) {
                FileInputStream fis = new FileInputStream(file);
                InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
                BufferedReader reader = new BufferedReader(isr);
                String line = reader.readLine();
                while (line != null) {
                    String str = escapeHTML(p, line);
                    out.print(str);
                    out.print("<br>");
                    line = reader.readLine();
                }
                reader.close();
            } else {
                out.print("File not found - " + servletPath.substring(5));
            }
        }
        out.print(String.join("\n", "</body>", "</html>"));
    }

    /**
     * HTML страница с выводом всех настроек сервиса валидации
     * 
     * @param contextPath URL сайта для формирования ссылок
     * @param resp        Поток для вывода результата
     * @throws IOException
     */
    private void get_sch_list(final String contextPath, HttpServletResponse resp)
            throws IOException {

        resp.setHeader("Content-Type", "text/html; charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.print(String.join("\n", "<html>", " <head>",
                "  <title>Список загруженных xsd, sch и сгенерированных xsl из sch</title>", " </head>", " <body>"));

        List<String> list = new ArrayList<String>();
        final File folder = new File(DATA_PATH);
        final File[] fl = folder.listFiles();
        if (fl != null) {
            for (final File fileEntry : fl) {
                if (fileEntry.isFile()) {
                    list.add(fileEntry.getName());
                } else if (fileEntry.isDirectory()) {
                    for (final File fileEntry2 : fileEntry.listFiles()) {
                        if (fileEntry2.isFile() && fileEntry2.getName().compareToIgnoreCase("CDA.xsd") == 0) {
                            list.add(fileEntry.getName() + "/CDA.xsd");
                        }
                    }
                }
            }
        } else {
            throw new IOException("Folder not found: " + DATA_PATH);
        }
        final File folder2 = new File(DATA_PATH + "/schematrons");
        final File[] fl2 = folder2.listFiles();
        if (fl2 != null) {
            for (final File fileEntry : fl2) {
                if (fileEntry.isFile()) {
                    list.add(fileEntry.getName());
                }
            }
        } else {
            throw new IOException("Folder not found: " + DATA_PATH + "/schematrons");
        }
        list.sort(Comparator.naturalOrder());

        out.print("<h2>Список загруженных xsd, sch и сгенерированных xsl из sch</h2>\n");
        if (LIST_TYPES_FOR_VARIFICATION.length > 0) {
            out.print(
                    "<h3>Включена выборочная проверка по типам <span style=\"font-weight:bold; background-color: #3366CC;\">"
                            + Arrays.toString(LIST_TYPES_FOR_VARIFICATION)
                            + "</span>. В списке выделены активные типы РЭМД</h3>\n");
        }
        out.print("<table><tr><th>File</th><th>Size in bytes</th><th>Upload date</th></tr>");
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy hh:mm:ss");
        for (Iterator<String> i = list.iterator(); i.hasNext();) {
            String item = i.next();
            String style = "";
            if (LIST_TYPES_FOR_VARIFICATION.length > 0) {
                String n = item.substring(0, item.length() - 4);
                if (n.endsWith("CDA") && item.length() > 8) {
                    n = item.substring(0, item.length() - 8); // 102/CDA.xsd
                } else {
                    int idx = n.indexOf('_');
                    if (idx > 0) {
                        n = n.substring(0, idx); // 102_1.sch
                    }
                }
                if (Arrays.binarySearch(LIST_TYPES_FOR_VARIFICATION, n) >= 0) {
                    style = " style=\"font-weight:bold; background-color: #3366CC;\"";
                }
            }
            String ext = item.substring(item.length() - 3, item.length());
            if (ext.equals("sch") || ext.equals("xsd")) {
                File file = new File(DATA_PATH + "/" + item);
                out.print(String.join("", "<tr><td", style, "><a href='./file/", item, "'>", item,
                        "</a></td><td align='right'", style, ">", Long.toString(file.length()), "</td><td", style, ">",
                        dateFormat.format(file.lastModified()), "</td></tr>\n"));
            } else if (ext.equals("xsl")) {
                File file = new File(DATA_PATH + "/schematrons/" + item);
                out.print(String.join("", "<tr><td", style, ">", "schematrons/" + item, "</td><td", style, "></td><td",
                        style, ">", dateFormat.format(file.lastModified()), "</td></tr>\n"));
            }
        }

        if (LIST_TAGS_FOR_VARIFICATION.size() > 0) {
            out.println("<H2>Список тегов, проверяемых на соответствие ФНСИ (FNSITags.txt)</H2>");
            out.println(String.join("<br>", LIST_TAGS_FOR_VARIFICATION));
        }
        out.print(
                String.join("\n", "<h2>См. также</h2>", "<a href='" + contextPath + "/send_sch.html'>Настройки</a><br>",
                        "<a href='" + contextPath + "/send_semd.html'>Валидация СЭМД</a><br>", "</body>", "</html>"));
    }

    /**
     * Выполнение запроса к справочнику ФНСИ. Ответ выдается в JSON в формате ФНСИ.
     * База справочников формируется в консольной утилите fnsi-downloaded и сохраняется 
     * в файловой системе. Ответы выдаются in-memory базой. См. класс Ref.
     * 
     * @param url  - Строка с параметрами запроса. Описание формата см. в
     *             RefManager.request(url)
     * @param resp - Поток для вывода результата
     * @throws IOException
     */
    private void data(final String url, HttpServletResponse resp)
            throws IOException {

        resp.setHeader("Content-Type", "application/json; charset=UTF-8");
        PrintWriter out = resp.getWriter();
        try {
            JSONObject json = rm.request(url);
            out.print(json.toString());
        } catch (SQLException ex) {
            log.error(ex);
            throw new IOException(ex);
        }
    }
    
    /**
     * HTML страница с формой для отправки СЭМД на валидацию
     * 
     * @param contextPath URL сайта для формирования ссылок
     * @param resp        Поток для вывода результата
     * @throws IOException
     */
    private void send_semd(final String contextPath, HttpServletResponse resp)
            throws IOException {

        resp.setHeader("Content-Type", "text/html; charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.print(String.join("\n", "<html>", "<head>", "  <title>Валидация СЭМД</title>", "</head>", "<body>",
                "<H1>Валидация СЭМД</H1>",
                "<form enctype = 'multipart/form-data' method='post' action='" + contextPath + "/verify'>",
                "  <fieldset>", "   <p><label for='remdtype'>", 
                "Тип РЭМД (OID):</label><input type='text' name='remdtype'></p>",
                "   <p><label for='verifytype'>Тип валидации. 0 - xsd, 1 - sch, 2 - xsd+sch, 3 - xsd+sch+fnsi :</label><input type='text' name='verifytype' value='3'></p>",
                "  </fieldset>", "  <p>", "   <input type='file' name='file'>",
                "   <input type='submit' value='Отправить СЭМД'>", "  </p>", "</form>", "<hr>", "<h2>См. также</h2>",
                "<a href='" + contextPath + "/send_sch.html'>Настройки</a><br>",
                "<a href='" + contextPath + "/get_sch_list.html'>Просмотр загруженных схем и схематронов</a><br>",
                "</body>", "</html>"));
    }

    /**
     * Разархивация zip файла
     * 
     * @param destinationFolder Каталог, куда будут сохранены файлы архива
     * @param zipFile           Исходный файл
     * @throws IOException
     */
    private static void unzip(final String destinationFolder, InputStream zipFile)
            throws IOException {

        ZipInputStream zipInput = new ZipInputStream(zipFile);
        ZipEntry entry = zipInput.getNextEntry();
        byte[] buffer = new byte[2048];

        while (entry != null) {
            final String entryName = entry.getName();
            final File file = new File(destinationFolder + File.separator + entryName);

            if (entry.isDirectory()) {
                Files.createDirectories(file.toPath());
            } else {
                FileOutputStream fOutput = new FileOutputStream(file);
                int count = 0;
                while ((count = zipInput.read(buffer)) > 0) {
                    fOutput.write(buffer, 0, count);
                }
                fOutput.close();
            }
            zipInput.closeEntry();
            entry = zipInput.getNextEntry();
        }
        zipInput.closeEntry();
        zipInput.close();
    }

    /**
     * getServletInfo<BR>
     * Required by Servlet interface
     */
    public String getServletInfo() {
        return "SEMD Validator";
    }

    /**
     * Выполнить xslt трасформацию для xml
     * 
     * @param source Поток с xml
     * @param style  Стилевой файл для выполнения трансформации
     * @param dst    Поток с результатом трансформации
     * @throws TransformerException
     */
    private void xslt(StreamSource source, final String style, StreamResult dst)
            throws TransformerException {

        final Templates pss = tryXsltCache(style);
        Transformer transformer = pss.newTransformer();
        transformer.transform(source, dst);
    }

    /**
     * Построить DOM для xml и выполнить проверку на соответствие схеме. Схема по
     * возможности берется из кеша
     * 
     * @param xml Строка с xml
     * @param xsd Путь к файлу со схемой
     * @return DOM для xml
     * @throws IOException
     * @throws ParserConfigurationException
     * @throws SAXException
     */
    private Document xsd(final String xml, final String xsd)
            throws IOException, ParserConfigurationException, SAXException {

        final Schema s = tryXsdCache(xsd);
        DocumentBuilderFactory builderFactory = DocumentBuilderFactory
                .newInstance("org.apache.xerces.jaxp.DocumentBuilderFactoryImpl", null);
        builderFactory.setNamespaceAware(true);
        builderFactory.setSchema(s);
        DocumentBuilder parser = builderFactory.newDocumentBuilder();
        InputStream stream = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
        XsdErrorHandler handler = new XsdErrorHandler();
        parser.setErrorHandler(handler);
        Document document = parser.parse(stream);
        String error = handler.getMessage();
        if (error.length() > 0) {
            throw new SAXException(error);
        }
        return document;
    }

    /**
     * Получить шаблон xslt трасформации по xsl. Если шаблон сохранен в кеше, то
     * взять из кеша. Иначе создается новый и сохраняется в кеше
     * 
     * @param path Путь к файлу с стилем странсформации
     * @return шаблон xslt трасформации
     * @throws TransformerException
     */
    private Templates tryXsltCache(final String path)
            throws TransformerException {

        Templates x;
        synchronized (cacheXslt) {
            x = cacheXslt.get(path);
            if (x == null) {
                TransformerFactory factory = TransformerFactory.newInstance();
                x = factory.newTemplates(new StreamSource(new File(path)));
                cacheXslt.put(path, x);
            }
        }
        return x;
    }

    /**
     * Получить схему. Если схема сохранена в кеше, то взять из кеша. Иначе
     * создается новая и сохраняется в кеше
     * 
     * @param path Путь к файлу со схемой
     * @return Схему для использования в процессе валидации
     * @throws SAXException
     */
    private Schema tryXsdCache(final String path)
            throws SAXException {

        Schema s;
        synchronized (cacheXsd) {
            s = cacheXsd.get(path);
            if (s == null) {
                SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
                s = sf.newSchema(new StreamSource(new File(path)));
                cacheXsd.put(path, s);
            }
        }
        return s;
    }

    /**
     * Выполнить трасформацию xsd схемы в html.
     * Директива <include> схемы преобразуется в гипер ссылку.
     * 
     * @param path Путь к сайту, используемый для формирования гипер ссылок
     * @param s    исходный xml
     * @return результирующий html
     */
    private static String escapeHTML(final String path, final String s) {
        StringBuilder out = new StringBuilder(Math.max(16, s.length()));
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c > 127 || c == '"' || c == '\'' || c == '<' || c == '>' || c == '&') {
                out.append("&#");
                out.append((int) c);
                out.append(';');
            } else if (c == ' ') {
                out.append("&nbsp;");
            } else {
                out.append(c);
            }
        }

        int i1 = out.indexOf("include");
        if (i1 > 0) {
            int i2 = out.indexOf("schemaLocation", i1 + 8);
            if (i2 >= 0) {
                int i3 = out.indexOf("&#39;", i2 + 15); // символ '
                if (i3 < 0) {
                    i3 = out.indexOf("&#34;", i2 + 15); // символ "
                }
                int i4 = out.indexOf("&#39;", i3 + 5); // символ '
                if (i4 < 0) {
                    i4 = out.indexOf("&#34;", i3 + 5); // символ "
                }
                if (i3 > 0 && i4 > 0) {
                    String file = out.substring(i3 + 5, i4);
                    String out2 = out.substring(0, i3 + 5) + "<a href='" + path + file + "'>" + file + "</a>"
                            + out.substring(i4);
                    return out2;
                }
            }
        }
        return out.toString();
    }

    /**
     * Загрузка файла со списком тегов СЭМД, подлежащих проверке на соответствие
     * справочникам ФНСИ
     * 
     * @param resp - поток для вывода результата проверки
     * @throws IOException
     */
    private void loadFNSITags(PrintWriter resp) throws IOException {
        synchronized (LIST_TAGS_FOR_VARIFICATION) {
            final File file = new File(DATA_PATH + "/FNSITags.txt");
            LIST_TAGS_FOR_VARIFICATION.clear();
            if (file.exists()) {
                FileInputStream fis = new FileInputStream(file);
                InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
                BufferedReader reader = new BufferedReader(isr);
                String line = reader.readLine();
                while (line != null) {
                    LIST_TAGS_FOR_VARIFICATION.add(line.trim());
                    line = reader.readLine();
                }
                reader.close();
            }
        }
    }

    /**
     * Вроверка тега на соответствие ФНСИ
     * 
     * @param resp       - поток для вывода результата проверки
     * @param tag        - какой тег сейчас проверяется
     * @param attributes - атрибуты проверяемого тега
     * @return возвращает true в случае успешной прохождения проверки, false при
     *         наличии ошибки
     */
    private boolean validateTag(PrintWriter resp, final String tag, final NamedNodeMap attributes,
            final String remdtype) {
        final org.w3c.dom.Node codeSystemN = attributes.getNamedItem("codeSystem");
        final org.w3c.dom.Node codeSystemVersionN = attributes.getNamedItem("codeSystemVersion");
        final org.w3c.dom.Node codeSystemNameN = attributes.getNamedItem("codeSystemName");
        final org.w3c.dom.Node codeN = attributes.getNamedItem("code");
        final org.w3c.dom.Node displayNameN = attributes.getNamedItem("displayName");
        if (codeSystemN == null || codeSystemNameN == null || codeN == null || displayNameN == null) {
            return true;
        }
        final String codeSystem = codeSystemN.getNodeValue();
        String codeSystemVersion = codeSystemVersionN.getNodeValue();
        final String codeSystemName = codeSystemNameN.getNodeValue();
        final String code = codeN.getNodeValue();
        final String displayName = displayNameN.getNodeValue();

        Ref ref;
        try {
            if (codeSystemVersion == null) { // если версия не указана, то надо взять последнюю
                codeSystemVersion = rm.getActualRefVersion(codeSystem);
                if (codeSystemVersion == null) {
                    resp.println(":WARNING: Tag-'" + tag + "'. Справочник OID [" + codeSystem
                            + "] отсутствует в справочнике 1.2.643.5.1.13.13.99.2.805");
                    return false;
                }
                String version2 = rm.getRefVersionForSemd(remdtype, codeSystem);
                if (version2 == null) {
                    resp.println(":WARNING: Tag-'" + tag + "'. Справочник OID [" + codeSystem
                            + "] отсутствует в справочнике 1.2.643.5.1.13.13.99.2.805 для типа РЭМД [" + remdtype
                            + "]");
                    return false;
                }
                if (!version2.equals(codeSystemVersion)) {
                    resp.println(":WARNING: Tag-'" + tag + "'. Справочник OID [" + codeSystem
                            + "], версия [не указана, актуальная " + codeSystemVersion
                            + "] не соотвествует справочнику 1.2.643.5.1.13.13.99.2.805. Необходимо использовать ["
                            + version2 + "], проверка проводится по этой версии");
                    codeSystemVersion = version2;
                }
            } else {
                String version2 = rm.getRefVersionForSemd(remdtype, codeSystem);
                if (version2 == null) {
                    resp.println(":WARNING: Tag-'" + tag + "'. Справочник OID [" + codeSystem
                            + "] отсутствует в справочнике 1.2.643.5.1.13.13.99.2.805 для типа РЭМД [" + remdtype
                            + "]");
                    return false;
                }
                if (!version2.equals(codeSystemVersion)) {
                    resp.println(":WARNING: Tag-'" + tag + "'. Справочник OID [" + codeSystem + "], версия ["
                            + codeSystemVersion
                            + "] не соотвествует справочнику 1.2.643.5.1.13.13.99.2.805. Необходимо использовать ["
                            + version2 + "], проверка проводится по этой версии");
                    codeSystemVersion = version2;
                }
            }

            ref = rm.getRef(codeSystem, codeSystemVersion);
            if (ref == null) {
                resp.println(":Internal error: Tag-'" + tag + "'. Справочник OID [" + codeSystem + "], версия ["
                        + codeSystemVersion + "] не загружен");
                return false;
            }
        } catch (IOException ex) {
            log.error(ex.getMessage(), ex);
            resp.println(":WARNING: Internal error" + ex.getMessage());
            ex.printStackTrace();
            return true;
        } catch (SQLException ex) {
            log.error(ex.getMessage(), ex);
            resp.println(":WARNING: Internal error - " + ex.getMessage());
            ex.printStackTrace();
            return true;
        }
        if (ref.validateRefName) {
            if (!codeSystemName.equals(ref.codeSystemName)) {
                resp.println(":ERROR: Tag-'" + tag + "'. Неверный codeSystemName. Справочник OID [" + codeSystem
                        + "], версия [" + codeSystemVersion + "]. '" + ref.codeSystemName + "' != '" + codeSystemName
                        + "'");
                return false;
            }
        }
        if (!ref.load) { // справочник помечен как незагружаемый, проверки прекращаем
            return true;
        }
        if (ref.isEmpty()) {
            resp.println(":WARNING: Tag-'" + tag + "'. Для справочника " + codeSystem + ", версия [" + codeSystemVersion
                    + "] '" + codeSystemName + "' не установлены KEY или VALUE в настройках FNSIlist.json");
            return true;
        }
        try {
            Node<RefData> val = ref.data.get(code);
            if (val == null) {
                resp.println(":ERROR: Tag-'" + tag + "'. INVALID_ELEMENT_VALUE_CODE Справочник OID [" + codeSystem
                        + "], версия [" + codeSystemVersion + "]. Элемент с кодом [" + code + "] отсутствует");
                return false;
            }
            if (!ref.validateValue)
                return true;
            String d2 = val.value.toString();
            if (!d2.equals(displayName)) {
                resp.println(":ERROR: Tag-'" + tag + "'. INVALID_ELEMENT_VALUE_NAME Справочник OID [" + codeSystem
                        + "], версия [" + codeSystemVersion + "]. Наименование элемента [" + displayName + "] с кодом ["
                        + code + "] не соответствует наименованию элемента в НСИ [" + d2 + "]");
                return false;
            }
            return true;
        } catch (RuntimeException ex) {
            log.error("Exception while validate-" + codeSystemN + "-" + codeSystemVersionN + " code-" + code + ". Ref-" + ref.toString());
            throw ex;
        }
    }
}
